Exchange rates are notoriously difficult to predict. Even extraplating from transaction costs its hard to find a model that proves more accurate than a random walk (mean zero). Many of the models learned in my Macrofinance class, 456, taught us models that rely on inaccessible data such as the natural interest rate and output gap. Sometimes these indictators are provided though are highly speculative and not widely availiable across countries.
While any complex model was out the question there was the simple real exchange rate equation defined as
$$RER = \frac{P_{US}}{P_{i}} * S $$Where the RER is the real exchange rate, S is the nominal exchange rate and P are the price levels in the respective counties. To clarify this is always in the US perspective i.e. for Canada and US an RER = 1.3 would mean for every 1 USD you get 1.3 CAD.
One theroy is that long run RER stays constant between counties so if the RER is above the mean in one year in the long run it should come back down and meaning the home currency will depriciate. Since the US is our benchmark a RER that is above the long run RER would mean the exchange rate should adjust down. In this example the USD feels a depriciation compared to the other country in question.
My main question was whether I could build a framework that made money on average following some investing rule based on this model. To test this I created a short and long betting strategy in the currency depending on what my model said. If my model predicted a depriciation in the the non-US currency I would take out a loan in the that currency using it to buy US dollars and hold those. Then after some period I would convert the currency back into the non US currency and make a profit! (or more likely a loss). The opposite trading strategy occured when I predicted the non-US currency would appriciate.
There are some obvious assumptions that need to be addressed. First I assumed the present value of my investment stayed the same over the years. This is highly unlikely because US interest rate is much lower than more risky currencies and loan rates will differ depending on the nation. With more time, and access to more data it would be interesting to relax this assumption and account for various lending rates between countries to take into considering the accumulation/depriciation of wealth over the holding period.
Another assumption was no trading costs and unlimited access to captial and credit. This assumption is less of an issue as it could easily be addressed by taking a percentage from the returns based on what counties were trading.
Im sorry in advance I tried to make this neater by importing it in but I got a ton of errors and five hours later determined I should probably be studying for another class.....just scroll until you see Latex!
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')
def data_comp(variables):
'''
cleaning OECD data pass in the name of csv files you are using
'''
for n, var in enumerate(variables):
#read in file OECD data
df = pd.read_csv(f"{var}.csv")
#drop redundent columns and transform it into Year, Country, Variable
df.columns = df.iloc[2]
df.drop([0,1,2], inplace=True)
df.drop(df.columns[1:44], axis=1, inplace=True)
df.set_index('Country Name', inplace = True, drop = True)
df = df.T
df.index = [round(idx) for idx in df.index]
df.index = pd.to_datetime(df.index, format='%Y')
df = df.reset_index()
df = pd.melt(df, id_vars=['index'], value_vars=df.columns[1:], var_name='Country Name', value_name='Value')
df.set_index(['index', 'Country Name'], inplace=True)
df.index.names = ["Year", "Country Name"]
df.rename(columns={'Value': var}, inplace=True)
#making the dataframe and combining over all variables
if n == 0:
full_df = df
else:
full_df = pd.concat([full_df, df], axis =1)
return(full_df)
df_PPP_model= data_comp(['RER', 'Forex rate', 'PPP'])
df_PPP = pd.read_csv(f"PPP_US.csv")
df_PPP_model = df_PPP_model[df_PPP_model.notna().all(axis=1)]
df_PPP_model['con_RER'] = (df_PPP_model['Forex rate'] * df_PPP_model['PPP'])
df =df_PPP_model.reset_index()
df= pd.read_csv(f"PPP_US.csv")
country_mapping = {
'ARM': 'Armenia', 'ATG': 'Antigua and Barbuda', 'AUS': 'Australia', 'BDI': 'Burundi', 'BGR': 'Bulgaria',
'BHR': 'Bahrain', 'BHS': 'Bahamas, The', 'BLZ': 'Belize', 'BOL': 'Bolivia', 'BRA': 'Brazil',
'CAF': 'Central African Republic', 'CAN': 'Canada', 'CHE': 'Switzerland', 'CHL': 'Chile', 'CHN': 'China',
'CIV': "Cote d'Ivoire", 'CMR': 'Cameroon', 'COD': 'Congo, Dem. Rep.', 'COL': 'Colombia', 'CRI': 'Costa Rica',
'CYP': 'Cyprus', 'CZE': 'Czechia', 'DMA': 'Dominica', 'DNK': 'Denmark', 'DOM': 'Dominican Republic',
'DZA': 'Algeria', 'FJI': 'Fiji', 'GAB': 'Gabon', 'GBR': 'United Kingdom', 'GEO': 'Georgia', 'GHA': 'Ghana',
'GMB': 'Gambia, The', 'GNQ': 'Equatorial Guinea', 'GRC': 'Greece', 'GRD': 'Grenada', 'GUY': 'Guyana',
'HKG': 'Hong Kong SAR, China', 'HRV': 'Croatia', 'HUN': 'Hungary', 'IRN': 'Iran, Islamic Rep.', 'ISL': 'Iceland',
'ISR': 'Israel', 'JPN': 'Japan', 'KNA': 'St. Kitts and Nevis', 'KOR': 'Korea, Rep.', 'LCA': 'St. Lucia',
'LSO': 'Lesotho', 'LVA': 'Latvia', 'MAR': 'Morocco', 'MDA': 'Moldova', 'MEX': 'Mexico', 'MKD': 'North Macedonia',
'MLT': 'Malta', 'MWI': 'Malawi', 'MYS': 'Malaysia', 'NGA': 'Nigeria', 'NIC': 'Nicaragua', 'NOR': 'Norway',
'NZL': 'New Zealand', 'PAK': 'Pakistan', 'PHL': 'Philippines', 'PNG': 'Papua New Guinea', 'POL': 'Poland',
'PRY': 'Paraguay', 'ROU': 'Romania', 'RUS': 'Russian Federation', 'SAU': 'Saudi Arabia', 'SGP': 'Singapore',
'SLB': 'Solomon Islands', 'SLE': 'Sierra Leone', 'SVK': 'Slovak Republic', 'SWE': 'Sweden', 'TGO': 'Togo',
'TTO': 'Trinidad and Tobago', 'TUN': 'Tunisia', 'UGA': 'Uganda', 'UKR': 'Ukraine', 'URY': 'Uruguay', 'USA': 'United States',
'VCT': 'St. Vincent and the Grenadines', 'VEN': 'Venezuela, RB', 'WSM': 'Samoa', 'ZAF': 'South Africa',
'ZMB': 'Zambia'}
def apply_country_mapping(location):
if location in country_mapping:
return country_mapping[location]
else:
return None
df['LOCATION'] = df['LOCATION'].apply(apply_country_mapping)
# Extract only the necessary columns
df = df[['LOCATION', 'TIME', 'Value']]
df['TIME'] = pd.to_datetime(df['TIME'], format='%Y')
df = df.rename(columns={'Value': 'PPP_for_ER'})
df = df.dropna()
df.set_index(['TIME', 'LOCATION'], inplace=True)
df.index.set_names(['Year', 'Country Name'], inplace=True)
df
df_merged = df.merge(df_PPP_model, left_index=True, right_index=True, how='inner')
df_merged['Final_diff'] = df_merged['con_RER'] - df_merged['PPP_for_ER']
#filtering out unstable ones, increases returns by only 2% in higher return one so not super important
df_merged = df_merged[df_merged['Final_diff']>-0.1]
df_merged = df_merged[df_merged['Final_diff']<0.1]
df_merged.sort_index(inplace=True)
# Select all rows with years before 2015 and including 2015
df_year_before_2015 = df_merged.loc[(slice(None, '2015-01-01'), slice(None)), :]
df_merged['diff_in_ave_RER'] = df_merged['RER'].div(df_year_before_2015.groupby('Country Name').mean()['RER'])
df_years={}
for yr in range(2000, 2022):
name = f'df_{yr}'
dt_year= pd.to_datetime(f'{yr}-01-01')
df_year = df_merged.loc[(dt_year, slice(None))]
df_years[yr] = df_year
For mean reversion I am taking the average of the historical $RER$ up until 2015 and then predicting on this 2015-2018 using a collection of 1-4 year holding periods. I calculed the difference in the average $RER$ as $$\frac{RER_{n}}{ \Sigma_{i=2000}^ {2014} \frac{1}{15}RER_i}$$ where n is between 2015-2018.
If this value was greater than one that means the USD is overvalued or other currency is undervalued as such we expect the USD to depreciate and should take out a loan in USD and buy the other currency so that is what I similated below.
# Function that determines if the currency is overvalued i.e. >1 and we should should bet the currency will go up
def holdings(row):
if row['diff_in_ave_RER'] > 1:
loan_amount = row['loan_amount'] * row['Forex rate']
row['usd_amount'] = loan_amount/row['Forex rate']
row['home_currency_amount'] = -loan_amount
else:
loan_amount = row['loan_amount']
row['usd_amount'] = -loan_amount
row['home_currency_amount'] = loan_amount * row['Forex rate']
return row
yr_past = 2015
returns = {}
for yr_past in range(2015,2018):
list = []
for yr in range(1,5):
yr_future = yr_past+yr
df = df_years[yr_past]
df['loan_amount'] = 1000
df_mean_RER_investment = df_years[yr_past].apply(holdings,axis=1)
df_mean_RER_investment[f'{yr_future}_ER'] = df_years[yr_future]['Forex rate']
df_mean_RER_investment['new_usd_amount'] = (1/df_mean_RER_investment[f'{yr_future}_ER'])*df_mean_RER_investment['home_currency_amount']
df_mean_RER_investment['return_USD'] = df_mean_RER_investment['new_usd_amount'] + df_mean_RER_investment['usd_amount']
total_investment = df_mean_RER_investment['return_USD'].count() *1000
gains = df_mean_RER_investment['return_USD'].sum()
return_to = (((gains+total_investment)/total_investment-1)*100).round(3)
list.append(return_to)
returns[yr_past] = list
returns_mean_reverting = []
for i in range(0,4):
elements = [v[i] for v in returns.values()]
average = sum(elements) / len(elements)
returns_mean_reverting.append(average)
print(f"Average Returns for {i+1} year holding period:", average.round(2))
Average Returns for 1 year holding period: 3.46 Average Returns for 2 year holding period: 6.04 Average Returns for 3 year holding period: 9.58 Average Returns for 4 year holding period: 12.89
There was OECD data on the reported real exchange rates, while there wasnt any information on how exactly they reported this is wasn't by our definition above as such I thought another interesting model would be to take the difference in their reports and the basic equation for $RER$ to give us the direction of investment. The PPP index is reported and the nominal exchange rate so I used those to calculate my predictied $RER$ which I denote con for constructed $RER$.
$$ RER_{con} = PPP*S $$Then I took the OECD reported RER to find a differential $$\Delta = RER_{con} - RER$$
I hypothesised that if this value was positive that would mean the dollar would stregthen (increasing the $RER$ reported by the OECD). While not rigourous I decided to stick to the theory that $RER_{con}$ is a constant and $RER$ is our changing value.
I first model this with equal investments in all countries based on a holdings rule
def holdings(row):
if row['Final_diff'] > 0:
loan_amount = 1000 * row['Forex rate']
row['usd_amount'] = loan_amount/row['Forex rate']
row['home_currency_amount'] = -loan_amount
else:
loan_amount = 1000
row['usd_amount'] = -loan_amount
row['home_currency_amount'] = loan_amount * row['Forex rate']
return row
returns = {}
for yr_past in range(2009,2018):
list = []
for yr in range(1,5):
yr_future = yr_past+yr
df_mean_PPP_diff_investment = df_years[yr_past].apply(holdings, axis=1)
df_mean_PPP_diff_investment[f'{yr_future}_ER'] = df_years[yr_future]['Forex rate']
df_mean_PPP_diff_investment['new_usd_amount'] = (1/df_mean_PPP_diff_investment[f'{yr_future}_ER'])*df_mean_PPP_diff_investment['home_currency_amount']
df_mean_PPP_diff_investment['return_USD'] = df_mean_PPP_diff_investment['new_usd_amount'] + df_mean_PPP_diff_investment['usd_amount']
total_investment = df_mean_PPP_diff_investment['return_USD'].count() *1000
gains = df_mean_PPP_diff_investment['return_USD'].sum()
return_to = (((gains+total_investment)/total_investment-1)*100).round(3)
list.append(return_to)
returns[yr_past] = list
returns_uniform = []
for i in range(0,4):
elements = [v[i] for v in returns.values()]
average = sum(elements) / len(elements)
returns_uniform.append(average)
print(f"Average Returns for {i+1} year holding period:", average.round(2))
Average Returns for 1 year holding period: 0.22 Average Returns for 2 year holding period: 0.52 Average Returns for 3 year holding period: 1.18 Average Returns for 4 year holding period: 1.83
You can see the average returns are positive and slightly increasing over the holding period which is what we want. To try and break my model and see whether we just got lucky over the ten years I am describing a new holding rule. Now you will invest in a currency proportional to your holdings so this will amplify our prediction rule effect. I capped investments at a threshold given some countries had abnormally high differentials as a non risk adverse invester I decided this was an okay assumption as having these outliers really skewed the returns.
def holdings(row):
if row['Final_diff'] > 0:
loan_amount = row['loan_amount']* row['Forex rate']
row['usd_amount'] = loan_amount/row['Forex rate']
row['home_currency_amount'] = -loan_amount
else:
loan_amount = row['loan_amount']
row['usd_amount'] = -loan_amount
row['home_currency_amount'] = loan_amount * row['Forex rate']
return row
returns = {}
for yr_past in range(2008,2018):
list = []
for yr in range(1,5):
yr_future = yr_past+yr
x = 10000000 * df_years[yr_past]['Final_diff']
df_years[yr_past]['loan_amount'] = x.clip(lower =-1000, upper=1000)
df_mean_PPP_diff_investment = df_years[yr_past].apply(holdings, axis=1)
df_mean_PPP_diff_investment[f'{yr_future}_ER'] = df_years[yr_future]['Forex rate']
df_mean_PPP_diff_investment['new_usd_amount'] = (1/df_mean_PPP_diff_investment[f'{yr_future}_ER'])*df_mean_PPP_diff_investment['home_currency_amount']
df_mean_PPP_diff_investment['return_USD'] = df_mean_PPP_diff_investment['new_usd_amount'] + df_mean_PPP_diff_investment['usd_amount']
total_investment = np.abs(df_mean_PPP_diff_investment['usd_amount']).sum()
gains = df_mean_PPP_diff_investment['return_USD'].sum()/total_investment *100
list.append(gains)
returns[yr_past] = list
returns_prop = []
for i in range(0,4):
elements = [v[i] for v in returns.values()]
average = sum(elements) / len(elements)
returns_prop.append(average)
print(f"Average Returns for {i+1} year holding period:", average.round(2))
Average Returns for 1 year holding period: 3.46 Average Returns for 2 year holding period: 6.04 Average Returns for 3 year holding period: 9.58 Average Returns for 4 year holding period: 12.89
Here our the returns for the different strategies graphed together, taking the average across all the years I modeled it for (2008-2018)
import plotly.graph_objs as go
# Create some data for the chart
x_values = [1, 2, 3, 4]
y_1 = returns_mean_reverting
y_2 = returns_uniform
y_3 = returns_prop
# Create two traces, one for each line
trace1 = go.Scatter(x=x_values, y=y_1, mode='lines', name='Average Returns to Mean Reverting Investing')
trace2 = go.Scatter(x=x_values, y=y_2, mode='lines', name='Average Returns Uniform PPP Investing')
trace3 = go.Scatter(x=x_values, y=y_3, mode='lines', name='Average Returns Proportional PPP Investing')
# Add the traces to a data object
data = [trace1, trace2, trace3]
# Define the layout for the chart
layout = go.Layout(title={'text': 'Returns to Different Investment Strategies','y': 0.95, 'x': 0.35, 'xanchor': 'center',
'yanchor': 'top'},
xaxis_title='Holding Period', yaxis_title='Percentage Return (%)')
# Create the figure
fig = go.Figure(data=data, layout=layout)
# Show the figure
fig.show()
To understand what is happending more clearly here is a snapshot of the proportional holding strategy showing the different holding periods and the returns depending on which year you started. It appears to be more reliable over longer time horizons.
import plotly.graph_objs as go
from plotly.subplots import make_subplots
# Define the years and values for each holding period
years = [k for k in returns.keys()]
values = [[v[i] for v in returns.values()] for i in range(4)]
# Define the colors for each holding period
colors = ['red', 'green', 'blue', 'purple']
# Define the subplot layout
fig = make_subplots(rows=2, cols=2, subplot_titles=["1 Year Holding Period", "2 Year Holding Period",
"3 Year Holding Period", "4 Year Holding Period"])
# Loop through the data and create a bar chart in each subplot
for i in range(4):
row = (i // 2) + 1
col = (i % 2) + 1
fig.add_trace(go.Bar(x=years, y=values[i], marker_color=colors[i], name=f'{i+1} Year Holding Period'),
row=row, col=col)
# Update the subplot layout and style
fig.update_layout(height=600, width=800,
title={
'text': 'Return in the PPP Strategy over Holding Periods by Start Year',
'y': 0.98,
'x': 0.5,
'xanchor': 'center',
'yanchor': 'top'},
showlegend=False,
margin=dict(l=20, r=20, t=80, b=20),
paper_bgcolor='rgb(255, 255, 255)',
plot_bgcolor='rgb(243, 243, 243)',
yaxis=dict(automargin=True),
xaxis=dict(automargin=True),
)
for i in range(2):
for j in range(2):
fig.update_xaxes(title_text="Year", row=i+1, col=j+1)
fig.update_yaxes(title_text="Returns (%)", row=i+1, col=j+1)
# Show the plot
fig.show()
Finally lets look at what we are doing, what currencies we are betting will go up and which we are betting to go down. The red indicates we are expecting the home currency to depriciate and took out an associated loan in that currency. Green indicates an appriciation in the currency so you want to loan in USD and hold in the other currency. The closer to white the hue is the less significant our differential is thus we took a smaller position. This map shows how your holdings change over the years (press replay if you want to see it go again).
import pandas as pd
import plotly.graph_objs as go
import plotly.offline as offline
import plotly.io as pio
pio.renderers.default = 'iframe'
df_full = []
colors = []
for year in range(2008, 2018):
df = df_years[year].reset_index()
df_full.append(df)
max = df['loan_amount'].max()
min = df['loan_amount'].min()
middle = 1- (max)/(max-min)
colors.append([[0, 'red'], [middle, 'white'], [1, 'green']])
frames = []
for i, year in enumerate(range(2008, 2018)):
# Create choropleth trace for each year
choropleth_trace = go.Choropleth(
locations=df_full[i]['Country Name'].astype(str),
z=df_full[i]['loan_amount'].astype(float),
locationmode='country names',
colorscale=colors[i],
colorbar_title="Home Currency t=0 ($)"
)
frames.append(go.Frame(data=[choropleth_trace]))
layout = go.Layout(
title={
'text': 'Betting: Shorting or Going Long 2008-2018',
'y': 0.9,
'x': 0.45,
'xanchor': 'center',
'yanchor': 'top'},
geo_scope='world',
updatemenus=[{
'type': 'buttons',
'buttons': [{
'label': 'Replay',
'method': 'animate',
'args': [None, {'frame': {'duration': 1000, 'redraw': True}, 'fromcurrent': True, 'transition': {'duration': 0, 'easing': 'linear'}}]
}]
}]
)
fig = go.Figure(data=[choropleth_trace], frames=frames)
fig.update_layout(layout)
offline.iplot(fig)
#sometimes it has issues redering if that is the case ask me for this file!
#offline.plot(fig, filename='animation.html')
Clearly a withen sample question but I was curious if there were any countries that we always took a long or short position on. Remember that we took a series of 10 years (and we can ignore the US as we never took a invested with it). Many of the countries or lower numbers like -2 or 2 which implies that 6:4 of the times we took on position over the other. Overall it appears that less developement is related to expecting exchange rate to increase which makes sense although no formal rule can we established.
values={}
yr = 2009
for country in df_years[yr].index:
value = 0
for yr in range(2008, 2018):
x = df_years[yr].groupby('Country Name')['Final_diff'].mean()
try:
if x[country]>=0:
value +=1
elif x[country]<0:
value -=1
except:
print()
values[country] = value
df = pd.Series(values)
df = df.reset_index()
colors = [[0, 'darkblue'], [0.37, 'white'], [1, 'green']]
# Choropleth map!
fig = go.Figure(data=go.Choropleth(
locations = df['index'].astype(str),
z = df[0].astype(float),
locationmode = 'country names',
colorscale = colors,
colorbar_title = "Home Currency t=0 ($)"
))
fig.update_layout(
#manually centering, not great but not sure how to automate
title={
'text': 'Performance of Different Currencies',
'y': 0.9,
'x': 0.45,
'xanchor': 'center',
'yanchor': 'top'},
geo_scope = 'world'
)
fig.show()
#offline.plot(fig, filename='static_map.html')
I thought this was pretty cool that we beat the market. There are a lot of questions I still have like how would this perform if trading costs were considered and holdings grew/depreicated with the respective safe asset/loan rates. One weakness of the model is that the US is always the base country so when it is hit by an unexpected period (like 2008 financial crisis) this causes it to the lose its assumed stablity. All in all I wouldn't start sinking your money into random currencies by this rule but I am curious how it will perform in the future with new data.